/* 
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
    distributed with this work for additional information
    regarding copyright ownership.  The ASF licenses this file
    to you under the Apache License, Version 2.0 (the
    "License"); you may not use this file except in compliance
    with the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing,
    software distributed under the License is distributed on an
    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    KIND, either express or implied.  See the License for the
    specific language governing permissions and limitations
    under the License.  
 */
package erwiki.part.sessionmonitor.internal.services;

import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Map;
import java.util.UUID;
import java.util.WeakHashMap;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionEvent;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.rap.rwt.RWT;
import org.eclipse.rap.rwt.service.UISession;
import org.elwiki.data.authorize.PrincipalComparator;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.ComponentFactory;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ServiceScope;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventAdmin;
import org.osgi.service.event.EventConstants;
import org.osgi.service.event.EventHandler;

import erwiki.api.WikiPartReference;
import erwiki.api.auth.AuthenticationManager;
import erwiki.api.auth.ISessionMonitor;
import erwiki.api.component.WikiComponent;
import erwiki.api.core.Engine;
import erwiki.api.core.WikiSession;
import erwiki.api.event.SecurityEvent;
import erwiki.api.event.WikiEvent;
import erwiki.api.exceptions.WikiException;
import erwiki.api.exceptions.WikiSecurityException;

/**
 * <p>
 * Управляет сессиями wiki для разных движков.
 * <p>
 * Сеансы хранятся как в HttpSession удаленного пользователя, так и в SessionMonitor.</br>
 * TODO: В JSPWiki этот класс был настроен как прослушиватель сеанса в файле web.xml для вики-сайта.
 */
//@formatter:off
@Component(
	name = "erwiki.SessionMonitor",
	service = { ISessionMonitor.class, WikiComponent.class, EventHandler.class },
	scope = ServiceScope.SINGLETON)
//@formatter:on
public final class SessionMonitor implements ISessionMonitor, WikiComponent, EventHandler {

	private static final Logger log = Logger.getLogger(SessionMonitor.class);

	private static ThreadLocal<WikiSession> c_guestSession = new ThreadLocal<>();

	/** Weak hashmap with HttpSessions as keys, and WikiSessions as values. */
	private final Map<String, WikiSession> m_sessions = new WeakHashMap<>();

	/**
	 * Construct the SessionListener
	 */
	public SessionMonitor() {
		// empty.
	}

	// -- OSGi service handling ----------------------(start)--

	@Reference
	volatile protected EventAdmin eventAdmin;

	@Reference(target = "(component.factory=elwiki.WikiSession.factory)")
	private ComponentFactory<WikiSession> factoryWikiSession;

	@WikiPartReference
	private AuthenticationManager authenticationManager;

	@WikiPartReference
	private Engine m_engine;

	@Activate
	protected void startup(BundleContext bc) {
		// nothing.
	}

	@Deactivate
	public void shutdown() {
		// TODO:
	}

	/** {@inheritDoc} */
	@Override
	public void initialize() throws WikiException {
		// nothong to do.
	}

	// -- OSGi service handling ------------------------(end)--

	/**
	 * Просто ищет WikiSession, ассоциированную с заданной HttpSession пользователя; не создает новую.
	 * Этот метод может возвращать значение <code>null</code>, <em>и вызывающие объекты должны проверять
	 * это значение</em>.
	 * <p>
	 * <b>NOTE:</b> работает без контекста RAP.
	 * 
	 * @param сессия HTTP-сеанса пользователя.
	 * @return WikiSession, если найдена.
	 */
	private WikiSession findSession(HttpSession session) {
		WikiSession wikiSession = null;
		String sessionId = (session == null) ? "(null)" : session.getId();
		WikiSession storedSession = m_sessions.get(sessionId);

		// Если слабая ссылка выдала вики-сессию, то возвратить ее.
		if (storedSession != null) {
			log.debug("Looking up WikiSession for session ID=" + sessionId + "... found it");
			wikiSession = storedSession;
		}

		return wikiSession;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public final void remove(final HttpServletRequest request) {
		if (request == null) {
			throw new IllegalArgumentException("Request cannot be null.");
		}
		remove(request.getSession().getId());
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public void remove(String wikiSessionId) {
		if (StringUtils.isBlank(wikiSessionId)) {
			throw new IllegalArgumentException("Incorrect Session Id: \"" + wikiSessionId + "\"");
		}
		synchronized (m_sessions) {
			WikiSession wikiSession = m_sessions.remove(wikiSessionId);
			c_guestSession.remove();
		}
	}

	/**
	 * {@inheritDoc}
	 */
	public int sessions() {
		return userPrincipals().length;
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public Principal[] userPrincipals() {
		final Collection<Principal> principals = new ArrayList<>();
		synchronized (m_sessions) {
			for (final WikiSession session : m_sessions.values()) {
				principals.add(session.getUserPrincipal());
			}
		}
		final Principal[] p = principals.toArray(new Principal[principals.size()]);
		Arrays.sort(p, PrincipalComparator.INSTANCE);
		return p;
	}

	/**
	 * {@inheritDoc}</br>
	 * Fires when the web container creates a new HTTP session.
	 */
	// @Override
	@Deprecated // в ErWiki - не требуется.
	public void sessionCreated(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		log.debug("Created Http Session: " + session.getId());
	}

	/**
	 * {@inheritDoc}</br>
	 * Removes the user's WikiSession from the internal session cache when the web container destroys an
	 * HTTP session.
	 */
	// @Override
	@Deprecated // в ErWiki - не требуется.
	public void sessionDestroyed(HttpSessionEvent se) {
		HttpSession session = se.getSession();
		WikiSession storedSession = this.findSession(session);
		this.remove(session.getId());
		log.debug("Removed Http Session: " + session.getId());
		if (storedSession != null) {
			eventAdmin.sendEvent(new Event(SecurityEvent.Topic.SESSION_EXPIRED,
					Map.of(SecurityEvent.PROPERTY_SESSION, storedSession)));
		}
	}

	/** {@inheritDoc} */
	@Override
	@Deprecated // не использовать этот метод! Следует использовать RWC.INSTANCE.getSession().
	public WikiSession getWikiSession() {
		UISession uiSession = RWT.getUISession();

		// :FVK: - для RSS. (перенести)
		if (uiSession == null) {
			if (log.isDebugEnabled()) {
				log.debug("Looking up WikiSession for NULL HttpRequest: returning createGuestSession()");
			}
			return staticGuestSession();
		}

		WikiSession wikiSession = getWikiSession(RWT.getRequest());

		// Настройка атрибутов.
		// TODO: Настройку атрибутов перенести в метод getWikiSession(HttpSession httpSession)
		wikiSession.setCachedLocale(uiSession.getLocale()); // :FVK: workaround?

		return wikiSession;
	}

	@Override
	public WikiSession createHeadlessSession() {
		String sessionId = UUID.randomUUID().toString();
		WikiSession wikiSession = this.createGuestSession(sessionId);
		return wikiSession;
	}

	/** {@inheritDoc} */
	@Override
	public WikiSession getWikiSession(@NonNull HttpServletRequest request) {
		Assert.isNotNull(request, "Incorrect parameter.");
		HttpSession httpSession = request.getSession();
		WikiSession wikiSession = findSession(httpSession);

		/* Если нет WikiSession - создается новый гостевой сеанс,
		 *  и устанавливается его ассоциация с HttpSession.
		 */
		if (wikiSession == null) {
			String sid = httpSession.getId();
			log.debug("Looking up WikiSession for session ID=" + sid + "... not found. Creating guestSession()");
			wikiSession = this.createGuestSession(sid);
			try {
				this.authenticationManager.login(request, wikiSession);
			} catch (WikiSecurityException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}

		return wikiSession;
	}

	/**
	 * {@inheritDoc}
	 * 
	 * @param sessionId
	 */
	@Override
	public WikiSession createGuestSession(String sessionId) {
		Dictionary<String, Object> properties = new Hashtable<String, Object>();
		if (sessionId != null) {
			properties.put(EventConstants.EVENT_FILTER, "(" + WikiEvent.PROPERTY_KEY_TARGET + "=" + sessionId + ")");
		}

		WikiSession wikiSession = (WikiSession) this.factoryWikiSession.newInstance(properties).getInstance();

		// Add the session as listener for GroupManager, AuthManager, AccountManager events
		// TODO: add listeners...
		/*
		//:FVK: final GroupManager groupMgr = Engine.getGroupManager();
		final IIAuthenticationManager authMgr = Engine.getAuthenticationManager();
		final AccountManager userMgr = Engine.getAccountManager();
		//:FVK: groupMgr.addWikiEventListener( session );
		authMgr.addWikiEventListener( session );
		userMgr.addWikiEventListener( session );
		 */

		synchronized (m_sessions) {
			m_sessions.put(sessionId, wikiSession);
		}
		
		return wikiSession;
	}

	/**
	 * Returns a static guest session, which is available for this thread only. This guest session is
	 * used internally whenever there is no HttpServletRequest involved, but the request is done e.g.
	 * when embedding JSPWiki code.
	 *
	 * @param engine Engine for this session
	 * @return A static WikiSession which is shared by all in this same Thread.
	 */
	// FIXME: Should really use WeakReferences to clean away unused sessions.
	// :FVK: Deprecated? не использовать?
	private WikiSession staticGuestSession() {
		WikiSession session = c_guestSession.get();
		if (session == null) {
			session = createGuestSession(null);
			c_guestSession.set(session);
		}

		return session;
	}

	@Override
	public String getSessionId(WikiSession session) {
		String sessionId = null;
		synchronized (m_sessions) {
			sessionId = m_sessions.entrySet().stream().filter(entry -> session.equals(entry.getValue()))
					.map(Map.Entry::getKey).findFirst().get();
		}
		return sessionId;
	}

	@Override
	public void handleEvent(Event event) {
		/*String topic = event.getTopic();
		switch (topic) {
			break;
		}*/
	}

}
